/**
 * @license jquery.panzoom.js v3.2.2
 * Updated: Wed May 08 2019
 * Add pan and zoom functionality to any element
 * Copyright (c) timmy willison
 * Released under the MIT license
 * https://github.com/timmywil/jquery.panzoom/blob/master/MIT-License.txt
 */

(function(global, factory) {
    // AMD
    if (typeof define === 'function' && define.amd) {
        define([ 'jquery' ], function(jQuery) {
            return factory(global, jQuery);
        });
        // CommonJS/Browserify
    } else if (typeof exports === 'object') {
        factory(global, require('jquery'));
        // Global
    } else {
        factory(global, global.jQuery);
    }
}(typeof window !== 'undefined' ? window : this, function(window, $) {
    'use strict';

    var document = window.document;
    var datakey = '__pz__';
    var slice = Array.prototype.slice;
    var rIE11 = /trident\/7./i;
    var supportsInputEvent = (function() {
        // IE11 returns a false positive
        if (rIE11.test(navigator.userAgent)) {
            return false;
        }
        var input = document.createElement('input');
        input.setAttribute('oninput', 'return');
        return typeof input.oninput === 'function';
    })();

    // Regex
    var rupper = /([A-Z])/g;
    var rsvg = /^http:[\w\.\/]+svg$/;

    var floating = '(\\-?\\d[\\d\\.e-]*)';
    var commaSpace = '\\,?\\s*';
    var rmatrix = new RegExp(
        '^matrix\\(' +
        floating + commaSpace +
        floating + commaSpace +
        floating + commaSpace +
        floating + commaSpace +
        floating + commaSpace +
        floating + '\\)$'
    );

    /**
     * Utility for determining transform matrix equality
     * Checks backwards to test translation first
     * @param {Array} first
     * @param {Array} second
     */
    function matrixEquals(first, second) {
        var i = first.length;
        while(--i) {
            if (Math.round(+first[i]) !== Math.round(+second[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Creates the options object for reset functions
     * @param {Boolean|Object} opts See reset methods
     * @returns {Object} Returns the newly-created options object
     */
    function createResetOptions(opts) {
        var options = { range: true, animate: true };
        if (typeof opts === 'boolean') {
            options.animate = opts;
        } else {
            $.extend(options, opts);
        }
        return options;
    }

    /**
     * Represent a transformation matrix with a 3x3 matrix for calculations
     * Matrix functions adapted from Louis Remi's jQuery.transform (https://github.com/louisremi/jquery.transform.js)
     * @param {Array|Number} a An array of six values representing a 2d transformation matrix
     */
    function Matrix(a, b, c, d, e, f, g, h, i) {
        if ($.type(a) === 'array') {
            this.elements = [
                +a[0], +a[2], +a[4],
                +a[1], +a[3], +a[5],
                0,     0,     1
            ];
        } else {
            this.elements = [
                a, b, c,
                d, e, f,
                g || 0, h || 0, i || 1
            ];
        }
    }

    Matrix.prototype = {
        /**
         * Multiply a 3x3 matrix by a similar matrix or a vector
         * @param {Matrix|Vector} matrix
         * @return {Matrix|Vector} Returns a vector if multiplying by a vector
         */
        x: function(matrix) {
            var isVector = matrix instanceof Vector;

            var a = this.elements,
                b = matrix.elements;

            if (isVector && b.length === 3) {
                // b is actually a vector
                return new Vector(
                    a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
                    a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
                    a[6] * b[0] + a[7] * b[1] + a[8] * b[2]
                );
            } else if (b.length === a.length) {
                // b is a 3x3 matrix
                return new Matrix(
                    a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
                    a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
                    a[0] * b[2] + a[1] * b[5] + a[2] * b[8],

                    a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
                    a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
                    a[3] * b[2] + a[4] * b[5] + a[5] * b[8],

                    a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
                    a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
                    a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
                );
            }
            return false; // fail
        },
        /**
         * Generates an inverse of the current matrix
         * @returns {Matrix}
         */
        inverse: function() {
            var d = 1 / this.determinant(),
                a = this.elements;
            return new Matrix(
                d * ( a[8] * a[4] - a[7] * a[5]),
                d * (-(a[8] * a[1] - a[7] * a[2])),
                d * ( a[5] * a[1] - a[4] * a[2]),

                d * (-(a[8] * a[3] - a[6] * a[5])),
                d * ( a[8] * a[0] - a[6] * a[2]),
                d * (-(a[5] * a[0] - a[3] * a[2])),

                d * ( a[7] * a[3] - a[6] * a[4]),
                d * (-(a[7] * a[0] - a[6] * a[1])),
                d * ( a[4] * a[0] - a[3] * a[1])
            );
        },
        /**
         * Calculates the determinant of the current matrix
         * @returns {Number}
         */
        determinant: function() {
            var a = this.elements;
            return a[0] * (a[8] * a[4] - a[7] * a[5]) - a[3] * (a[8] * a[1] - a[7] * a[2]) + a[6] * (a[5] * a[1] - a[4] * a[2]);
        }
    };

    /**
     * Create a vector containing three values
     */
    function Vector(x, y, z) {
        this.elements = [ x, y, z ];
    }

    /**
     * Get the element at zero-indexed index i
     * @param {Number} i
     */
    Vector.prototype.e = Matrix.prototype.e = function(i) {
        return this.elements[ i ];
    };

    /**
     * Create a Panzoom object for a given element
     * @constructor
     * @param {Element} elem - Element to use pan and zoom
     * @param {Object} [options] - An object literal containing options to override default options
     *  (See Panzoom.defaults for ones not listed below)
     * @param {jQuery} [options.$zoomIn] - zoom in buttons/links collection (you can also bind these yourself
     *  e.g. $button.on('click', function(e) { e.preventDefault(); $elem.panzoom('zoomIn'); });)
     * @param {jQuery} [options.$zoomOut] - zoom out buttons/links collection on which to bind zoomOut
     * @param {jQuery} [options.$zoomRange] - zoom in/out with this range control
     * @param {jQuery} [options.$reset] - Reset buttons/links collection on which to bind the reset method
     * @param {Function} [options.on[Start|Change|Zoom|Pan|End|Reset] - Optional callbacks for panzoom events
     */
    function Panzoom(elem, options) {

        // Allow instantiation without `new` keyword
        if (!(this instanceof Panzoom)) {
            return new Panzoom(elem, options);
        }

        // Sanity checks
        if (elem.nodeType !== 1) {
            $.error('Panzoom called on non-Element node');
        }
        if (!$.contains(document, elem)) {
            $.error('Panzoom element must be attached to the document');
        }

        // Don't remake
        var d = $.data(elem, datakey);
        if (d) {
            return d;
        }

        // Extend default with given object literal
        // Each instance gets its own options
        this.options = options = $.extend({}, Panzoom.defaults, options);
        this.elem = elem;
        var $elem = this.$elem = $(elem);
        this.$set = options.$set && options.$set.length ? options.$set : $elem;
        this.$doc = $(elem.ownerDocument || document);
        this.$parent = $elem.parent();
        this.parent = this.$parent[0];

        // This is SVG if the namespace is SVG
        // However, while <svg> elements are SVG, we want to treat those like other elements
        this.isSVG = rsvg.test(elem.namespaceURI) && elem.nodeName.toLowerCase() !== 'svg';

        this.panning = false;

        // Save the original transform value
        // Save the prefixed transform style key
        // Set the starting transform
        this._buildTransform();

        // Build the appropriately-prefixed transform style property name
        // De-camelcase
        this._transform = $.cssProps.transform ? $.cssProps.transform.replace(rupper, '-$1').toLowerCase() : 'transform';

        // Build the transition value
        this._buildTransition();

        // Build containment dimensions
        this.resetDimensions();

        // Add zoom and reset buttons to `this`
        var $empty = $();
        var self = this;
        $.each([ '$zoomIn', '$zoomOut', '$zoomRange', '$reset' ], function(i, name) {
            self[ name ] = options[ name ] || $empty;
        });

        this.enable();

        this.scale = this.getMatrix()[0];
        this._checkPanWhenZoomed();

        // Save the instance
        $.data(elem, datakey, this);
    }

    // Attach regex for possible use (immutable)
    Panzoom.rmatrix = rmatrix;

    Panzoom.defaults = {
        // Should always be non-empty
        // Used to bind jQuery events without collisions
        // A guid is not added here as different instantiations/versions of panzoom
        // on the same element is not supported, so don't do it.
        eventNamespace: '.panzoom',

        // Whether or not to transition the scale
        transition: true,

        // Default cursor style for the element
        cursor: 'move',

        // There may be some use cases for zooming without panning or vice versa
        disablePan: false,
        disableZoom: false,

        // Pan only on the X or Y axes
        disableXAxis: false,
        disableYAxis: false,

        // Set whether you'd like to pan on left (1), middle (2), or right click (3)
        which: 1,

        // The increment at which to zoom
        // Should be a number greater than 0
        increment: 0.3,

        // When no scale is passed, this option tells
        // the `zoom` method to increment
        // the scale *linearly* based on the increment option.
        // This often ends up looking like very little happened at larger zoom levels.
        // The default is to multiply/divide the scale based on the increment.
        linearZoom: false,

        // Pan only when the scale is greater than minScale
        panOnlyWhenZoomed: false,

        // min and max zoom scales
        minScale: 0.3,
        maxScale: 6,

        // The default step for the range input
        // Precendence: default < HTML attribute < option setting
        rangeStep: 0.05,

        // Animation duration (ms)
        duration: 200,
        // CSS easing used for scale transition
        easing: 'ease-in-out',

        // Indicate that the element should be contained within it's parent when panning
        // Note: this does not affect zooming outside of the parent
        // Set this value to 'invert' to only allow panning outside of the parent element (basically the opposite of the normal use of contain)
        // 'invert' is useful for a large panzoom element where you don't want to show anything behind it
        contain: false
    };

    Panzoom.prototype = {
        constructor: Panzoom,

        /**
         * @returns {Panzoom} Returns the instance
         */
        instance: function() {
            return this;
        },

        /**
         * Enable or re-enable the panzoom instance
         */
        enable: function() {
            // Unbind first
            this._initStyle();
            this._bind();
            this.disabled = false;
        },

        /**
         * Disable panzoom
         */
        disable: function() {
            this.disabled = true;
            this._resetStyle();
            this._unbind();
        },

        /**
         * @returns {Boolean} Returns whether the current panzoom instance is disabled
         */
        isDisabled: function() {
            return this.disabled;
        },

        /**
         * Destroy the panzoom instance
         */
        destroy: function() {
            this.disable();
            $.removeData(this.elem, datakey);
        },

        /**
         * Builds the restricing dimensions from the containment element
         * Also used with focal points
         * Call this method whenever the dimensions of the element or parent are changed
         */
        resetDimensions: function() {
            // Reset container properties
            this.container = this.parent.getBoundingClientRect();

            // Set element properties
            var elem = this.elem;
            // getBoundingClientRect() works with SVG, offsetWidth does not
            var dims = elem.getBoundingClientRect();
            var absScale = Math.abs(this.scale);
            this.dimensions = {
                width: dims.width,
                height: dims.height,
                left: $.css(elem, 'left', true) || 0,
                top: $.css(elem, 'top', true) || 0,
                // Borders and margins are scaled
                border: {
                    top: $.css(elem, 'borderTopWidth', true) * absScale || 0,
                    bottom: $.css(elem, 'borderBottomWidth', true) * absScale || 0,
                    left: $.css(elem, 'borderLeftWidth', true) * absScale || 0,
                    right: $.css(elem, 'borderRightWidth', true) * absScale || 0
                },
                margin: {
                    top: $.css(elem, 'marginTop', true) * absScale || 0,
                    left: $.css(elem, 'marginLeft', true) * absScale || 0
                }
            };
        },

        /**
         * Return the element to it's original transform matrix
         * @param {Boolean} [options] If a boolean is passed, animate the reset (default: true). If an options object is passed, simply pass that along to setMatrix.
         * @param {Boolean} [options.silent] Silence the reset event
         */
        reset: function(options) {
            options = createResetOptions(options);
            // Reset the transform to its original value
            var matrix = this.setMatrix(this._origTransform, options);
            if (!options.silent) {
                this._trigger('reset', matrix);
            }
        },

        /**
         * Only resets zoom level
         * @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to zoom()
         */
        resetZoom: function(options) {
            options = createResetOptions(options);
            var origMatrix = this.getMatrix(this._origTransform);
            options.dValue = origMatrix[ 3 ];
            this.zoom(origMatrix[0], options);
        },

        /**
         * Only reset panning
         * @param {Boolean|Object} [options] Whether to animate the reset (default: true) or an object of options to pass to pan()
         */
        resetPan: function(options) {
            var origMatrix = this.getMatrix(this._origTransform);
            this.pan(origMatrix[4], origMatrix[5], createResetOptions(options));
        },

        /**
         * Sets a transform on the $set
         * For SVG, the style attribute takes precedence
         * and allows us to animate
         * @param {String} transform
         */
        setTransform: function(transform) {
            var $set = this.$set;
            var i = $set.length;
            while(i--) {
                $.style($set[i], 'transform', transform);

                // Support IE9-11, Edge 13-14+
                // Set attribute alongside style attribute
                // since IE and Edge do not respect style settings on SVG
                // See https://css-tricks.com/transforms-on-svg-elements/
                if (this.isSVG) {
                    $set[i].setAttribute('transform', transform);
                }
            }
        },

        /**
         * Retrieving the transform is different for SVG
         *  (unless a style transform is already present)
         * Uses the $set collection for retrieving the transform
         * @param {String} [transform] Pass in an transform value (like 'scale(1.1)')
         *  to have it formatted into matrix format for use by Panzoom
         * @returns {String} Returns the current transform value of the element
         */
        getTransform: function(transform) {
            var $set = this.$set;
            var transformElem = $set[0];
            if (transform) {
                this.setTransform(transform);
            } else {

                // IE and Edge still set the transform style properly
                // They just don't render it on SVG
                // So we get a correct value here
                transform = $.style(transformElem, 'transform');

                if (this.isSVG && (!transform || transform === 'none')) {
                    transform = $.attr(transformElem, 'transform') || 'none';
                }
            }

            // Convert any transforms set by the user to matrix format
            // by setting to computed
            if (transform !== 'none' && !rmatrix.test(transform)) {

                // Get computed and set for next time
                this.setTransform(transform = $.css(transformElem, 'transform'));
            }

            return transform || 'none';
        },

        /**
         * Retrieve the current transform matrix for $elem (or turn a transform into it's array values)
         * @param {String} [transform] matrix-formatted transform value
         * @returns {Array} Returns the current transform matrix split up into it's parts, or a default matrix
         */
        getMatrix: function(transform) {
            var matrix = rmatrix.exec(transform || this.getTransform());
            if (matrix) {
                matrix.shift();
            }
            return matrix || [ 1, 0, 0, 1, 0, 0 ];
        },

        /**
         * Get the current scale.
         * @param {String} [transform] matrix-formatted transform value
         * @returns {Number} Current scale relative to the initial scale (height / width = 1)
         */
        getScale: function(matrix) {
            return Math.sqrt(Math.pow(matrix[0], 2) + Math.pow(matrix[1], 2));
        },

        /**
         * Given a matrix object, quickly set the current matrix of the element
         * @param {Array|String} matrix
         * @param {Object} [options]
         * @param {Boolean|String} [options.animate] Whether to animate the transform change, or 'skip' indicating that it is unnecessary to set
         * @param {Boolean} [options.contain] Override the global contain option
         * @param {Boolean} [options.range] If true, $zoomRange's value will be updated.
         * @param {Boolean} [options.silent] If true, the change event will not be triggered
         * @returns {Array} Returns the newly-set matrix
         */
        setMatrix: function(matrix, options) {
            if (this.disabled) { return; }
            if (!options) { options = {}; }
            // Convert to array
            if (typeof matrix === 'string') {
                matrix = this.getMatrix(matrix);
            }
            var scale = this.getScale(matrix);
            var contain = typeof options.contain !== 'undefined' ? options.contain : this.options.contain;

            // Apply containment
            if (contain) {
                var dims = options.dims;
                if (!dims) {
                    this.resetDimensions();
                    dims = this.dimensions;
                }
                var spaceWLeft, spaceWRight, scaleDiff;
                var container = this.container;
                var width = dims.width;
                var height = dims.height;
                var conWidth = container.width;
                var conHeight = container.height;
                var zoomAspectW = conWidth / width;
                var zoomAspectH = conHeight / height;

                // If the element is not naturally centered,
                // assume full space right
                if (this.$parent.css('textAlign') !== 'center' || $.css(this.elem, 'display') !== 'inline') {
                    // offsetWidth gets us the width without the transform
                    scaleDiff = (width - this.elem.offsetWidth) / 2;
                    spaceWLeft = scaleDiff - dims.border.left;
                    spaceWRight = width - conWidth - scaleDiff + dims.border.right;
                } else {
                    spaceWLeft = spaceWRight = ((width - conWidth) / 2);
                }
                var spaceHTop = ((height - conHeight) / 2) + dims.border.top;
                var spaceHBottom = ((height - conHeight) / 2) - dims.border.top - dims.border.bottom;

                if (contain === 'invert' || contain === 'automatic' && zoomAspectW < 1.01) {
                    matrix[4] = Math.max(Math.min(matrix[4], spaceWLeft - dims.border.left), -spaceWRight);
                } else {
                    matrix[4] = Math.min(Math.max(matrix[4], spaceWLeft), -spaceWRight);
                }

                if (contain === 'invert' || (contain === 'automatic' && zoomAspectH < 1.01)) {
                    matrix[5] = Math.max(Math.min(matrix[5], spaceHTop - dims.border.top), -spaceHBottom);
                } else {
                    matrix[5] = Math.min(Math.max(matrix[5], spaceHTop), -spaceHBottom);
                }
            }

            // Animate
            if (options.animate !== 'skip') {
                // Set transition
                this.transition(!options.animate);
            }

            // Update range element
            if (options.range) {
                this.$zoomRange.val(scale);
            }

            // Set the matrix on this.$set
            if (this.options.disableXAxis || this.options.disableYAxis) {
                var originalMatrix = this.getMatrix();
                if (this.options.disableXAxis) {
                    matrix[4] = originalMatrix[4];
                }
                if (this.options.disableYAxis) {
                    matrix[5] = originalMatrix[5];
                }
            }
            this.setTransform('matrix(' + matrix.join(',') + ')');

            this.scale = scale;

            // Disable/enable panning if zooming is at minimum and panOnlyWhenZoomed is true
            this._checkPanWhenZoomed(scale);

            if (!options.silent) {
                this._trigger('change', matrix);
            }

            return matrix;
        },

        /**
         * @returns {Boolean} Returns whether the panzoom element is currently being dragged
         */
        isPanning: function() {
            return this.panning;
        },

        /**
         * Apply the current transition to the element, if allowed
         * @param {Boolean} [off] Indicates that the transition should be turned off
         */
        transition: function(off) {
            if (!this._transition) { return; }
            var transition = off || !this.options.transition ? 'none' : this._transition;
            var $set = this.$set;
            var i = $set.length;
            while(i--) {
                // Avoid reflows when zooming
                if ($.style($set[i], 'transition') !== transition) {
                    $.style($set[i], 'transition', transition);
                }
            }
        },

        /**
         * Pan the element to the specified translation X and Y
         * Note: this is not the same as setting jQuery#offset() or jQuery#position()
         * @param {Number} x
         * @param {Number} y
         * @param {Object} [options] These options are passed along to setMatrix
         * @param {Array} [options.matrix] The matrix being manipulated (if already known so it doesn't have to be retrieved again)
         * @param {Boolean} [options.silent] Silence the pan event. Note that this will also silence the setMatrix change event.
         * @param {Boolean} [options.relative] Make the x and y values relative to the existing matrix
         */
        pan: function(x, y, options) {
            if (this.options.disablePan) { return; }
            if (!options) { options = {}; }
            var matrix = options.matrix;
            if (!matrix) {
                matrix = this.getMatrix();
            }
            // Cast existing matrix values to numbers
            if (options.relative) {
                x += +matrix[4];
                y += +matrix[5];
            }
            matrix[4] = x;
            matrix[5] = y;
            this.setMatrix(matrix, options);
            if (!options.silent) {
                this._trigger('pan', matrix[4], matrix[5]);
            }
        },

        /**
         * Zoom in/out the element using the scale properties of a transform matrix
         * @param {Number|Boolean} [scale] The scale to which to zoom or a boolean indicating to transition a zoom out
         * @param {Object} [opts] All global options can be overwritten by this options object. For example, override the default increment.
         * @param {Boolean} [opts.noSetRange] Specify that the method should not set the $zoomRange value (as is the case when $zoomRange is calling zoom on change)
         * @param {jQuery.Event|Object} [opts.focal] A focal point on the panzoom element on which to zoom.
         *  If an object, set the clientX and clientY properties to the position relative to the parent
         * @param {Boolean} [opts.animate] Whether to animate the zoom (defaults to true if scale is not a number, false otherwise)
         * @param {Boolean} [opts.silent] Silence the zoom event
         * @param {Array} [opts.matrix] Optionally pass the current matrix so it doesn't need to be retrieved
         * @param {Number} [opts.dValue] Think of a transform matrix as four values a, b, c, d
         *  where a/d are the horizontal/vertical scale values and b/c are the skew values
         *  (5 and 6 of matrix array are the tx/ty transform values).
         *  Normally, the scale is set to both the a and d values of the matrix.
         *  This option allows you to specify a different d value for the zoom.
         *  For instance, to flip vertically, you could set -1 as the dValue.
         */
        zoom: function(scale, opts) {
            // Shuffle arguments
            if (typeof scale === 'object') {
                opts = scale;
                scale = null;
            } else if (!opts) {
                opts = {};
            }
            var options = $.extend({}, this.options, opts);
            // Check if disabled
            if (options.disableZoom) { return; }
            var animate = false;
            var matrix = options.matrix || this.getMatrix();
            var surfaceM = new Matrix(matrix);
            var startScale = this.getScale(matrix);

            // Calculate zoom based on increment
            if (typeof scale !== 'number') {
                if (options.linearZoom) {
                    scale = 1 + (options.increment * (scale ? -1 : 1)) / startScale;
                } else {
                    scale = scale ? (1 / (1 + options.increment)) : (1 + options.increment);
                }
                animate = true;
            } else {
                scale = 1 / startScale;
            }

            // Constrain scale
            scale = Math.max(Math.min(scale, options.maxScale / startScale), options.minScale / startScale);
            var m = surfaceM.x(new Matrix(scale, 0, 0, 0, (typeof options.dValue === 'number' ? options.dValue / startScale : scale), 0));

            // Calculate focal point based on scale
            var focal = options.focal;
            if (focal && !options.disablePan) {
                // Adapted from code by Florian Günther
                // https://github.com/florianguenther/zui53
                this.resetDimensions();
                var dims = options.dims = this.dimensions;
                var clientX = focal.clientX;
                var clientY = focal.clientY;

                // Adjust the focal point for transform-origin 50% 50%
                // SVG elements have a transform origin of 0 0
                if (!this.isSVG) {
                    clientX -= (dims.width / startScale) / 2;
                    clientY -= (dims.height / startScale) / 2;
                }

                var clientV = new Vector(clientX, clientY, 1);
                // Supply an offset manually if necessary
                var o = this.parentOffset || this.$parent.offset();
                var offsetM = new Matrix(1, 0, o.left - this.$doc.scrollLeft(), 0, 1, o.top - this.$doc.scrollTop());
                var surfaceV = surfaceM.inverse().x(offsetM.inverse().x(clientV));
                surfaceM = surfaceM.x(new Matrix([scale, 0, 0, scale, 0, 0]));
                clientV = offsetM.x(surfaceM.x(surfaceV));
                matrix[4] = +matrix[4] + (clientX - clientV.e(0));
                matrix[5] = +matrix[5] + (clientY - clientV.e(1));
            }

            // Set the scale
            matrix[0] = m.e(0);
            matrix[1] = m.e(3);
            matrix[2] = m.e(1);
            matrix[3] = m.e(4);

            // Calling zoom may still pan the element
            this.setMatrix(matrix, {
                animate: typeof options.animate !== 'undefined' ? options.animate : animate,
                // Set the zoomRange value
                range: !options.noSetRange
            });

            // Trigger zoom event
            if (!options.silent) {
                this._trigger('zoom', scale, options);
            }
        },

        /**
         * Get/set option on an existing instance
         * @returns {Array|undefined} If getting, returns an array of all values
         *   on each instance for a given key. If setting, continue chaining by returning undefined.
         */
        option: function(key, value) {
            var options;
            if (!key) {
                // Avoids returning direct reference
                return $.extend({}, this.options);
            }

            if (typeof key === 'string') {
                if (arguments.length === 1) {
                    return this.options[ key ] !== undefined ?
                        this.options[ key ] :
                        null;
                }
                options = {};
                options[ key ] = value;
            } else {
                options = key;
            }

            this._setOptions(options);
        },

        /**
         * Internally sets options
         * @param {Object} options - An object literal of options to set
         * @private
         */
        _setOptions: function(options) {
            $.each(options, $.proxy(function(key, value) {
                switch(key) {
                    case 'disablePan':
                        this._resetStyle();
                    /* falls through */
                    case '$zoomIn':
                    case '$zoomOut':
                    case '$zoomRange':
                    case '$reset':
                    case 'disableZoom':
                    case 'onStart':
                    case 'onChange':
                    case 'onZoom':
                    case 'onPan':
                    case 'onEnd':
                    case 'onReset':
                    case 'eventNamespace':
                        this._unbind();
                }
                this.options[ key ] = value;
                switch(key) {
                    case 'disablePan':
                        this._initStyle();
                    /* falls through */
                    case '$zoomIn':
                    case '$zoomOut':
                    case '$zoomRange':
                    case '$reset':
                        // Set these on the instance
                        this[ key ] = value;
                    /* falls through */
                    case 'disableZoom':
                    case 'onStart':
                    case 'onChange':
                    case 'onZoom':
                    case 'onPan':
                    case 'onEnd':
                    case 'onReset':
                    case 'eventNamespace':
                        this._bind();
                        break;
                    case 'cursor':
                        $.style(this.elem, 'cursor', value);
                        break;
                    case 'minScale':
                        this.$zoomRange.attr('min', value);
                        break;
                    case 'maxScale':
                        this.$zoomRange.attr('max', value);
                        break;
                    case 'rangeStep':
                        this.$zoomRange.attr('step', value);
                        break;
                    case 'startTransform':
                        this._buildTransform();
                        break;
                    case 'duration':
                    case 'easing':
                        this._buildTransition();
                    /* falls through */
                    case 'transition':
                        this.transition();
                        break;
                    case 'panOnlyWhenZoomed':
                        this._checkPanWhenZoomed();
                        break;
                    case '$set':
                        if (value instanceof $ && value.length) {
                            this.$set = value;
                            // Reset styles
                            this._initStyle();
                            this._buildTransform();
                        }
                }
            }, this));
        },

        /**
         * Disable/enable panning depending on whether the current scale
         * matches the minimum
         * @param {Number} [scale]
         * @private
         */
        _checkPanWhenZoomed: function(scale) {
            var options = this.options;
            if (options.panOnlyWhenZoomed) {
                if (!scale) {
                    scale = this.getMatrix()[0];
                }
                var toDisable = scale <= options.minScale;
                if (options.disablePan !== toDisable) {
                    this.option('disablePan', toDisable);
                }
            }
        },

        /**
         * Initialize base styles for the element and its parent
         * @private
         */
        _initStyle: function() {
            var styles = {
                // Set the same default whether SVG or HTML
                // transform-origin cannot be changed to 50% 50% in IE9-11 or Edge 13-14+
                'transform-origin': this.isSVG ? '0 0' : '50% 50%'
            };
            // Set elem styles
            if (!this.options.disablePan) {
                styles.cursor = this.options.cursor;
            }
            this.$set.css(styles);

            // Set parent to relative if set to static
            var $parent = this.$parent;
            // No need to add styles to the body
            if ($parent.length && !$.nodeName(this.parent, 'body')) {
                styles = {
                    overflow: 'hidden'
                };
                if ($parent.css('position') === 'static') {
                    styles.position = 'relative';
                }
                $parent.css(styles);
            }
        },

        /**
         * Undo any styles attached in this plugin
         * @private
         */
        _resetStyle: function() {
            this.$elem.css({
                'cursor': '',
                'transition': ''
            });
            this.$parent.css({
                'overflow': '',
                'position': ''
            });
        },

        /**
         * Binds all necessary events
         * @private
         */
        _bind: function() {
            var self = this;
            var options = this.options;
            var ns = options.eventNamespace;
            var str_down = 'mousedown' + ns + ' pointerdown' + ns + ' MSPointerDown' + ns;
            var str_start = 'touchstart' + ns + ' ' + str_down;
            var str_click = 'touchend' + ns + ' click' + ns + ' pointerup' + ns + ' MSPointerUp' + ns;
            var events = {};
            var $reset = this.$reset;
            var $zoomRange = this.$zoomRange;

            // Bind panzoom events from options
            $.each([ 'Start', 'Change', 'Zoom', 'Pan', 'End', 'Reset' ], function() {
                var m = options[ 'on' + this ];
                if ($.isFunction(m)) {
                    events[ 'panzoom' + this.toLowerCase() + ns ] = m;
                }
            });

            // Bind $elem drag and click/touchdown events
            // Bind touchstart if either panning or zooming is enabled
            if (!options.disablePan || !options.disableZoom) {
                events[ str_start ] = function(e) {
                    var touches;
                    if (e.type === 'touchstart' ?
                            // Touch
                            (touches = e.touches || e.originalEvent.touches) &&
                            ((touches.length === 1 && !options.disablePan) || touches.length === 2) :
                            // Mouse/Pointer: Ignore unexpected click types
                            // Support: IE10 only
                            // IE10 does not support e.button for MSPointerDown, but does have e.which
                            !options.disablePan && (e.which || e.originalEvent.which) === options.which) {

                        e.preventDefault();
                        e.stopPropagation();
                        self._startMove(e, touches);
                    }
                };
                // Prevent the contextmenu event
                // if we're binding to right-click
                if (options.which === 3) {
                    events.contextmenu = false;
                }
            }
            this.$elem.on(events);

            // Bind reset
            if ($reset.length) {
                $reset.on(str_click, function(e) {
                    e.preventDefault();
                    self.reset();
                });
            }

            // Set default attributes for the range input
            if ($zoomRange.length) {
                $zoomRange.attr({
                    // Only set the range step if explicit or
                    // set the default if there is no attribute present
                    step: options.rangeStep === Panzoom.defaults.rangeStep &&
                    $zoomRange.attr('step') ||
                    options.rangeStep,
                    min: options.minScale,
                    max: options.maxScale
                }).prop({
                    value: this.getMatrix()[0]
                });
            }

            // No bindings if zooming is disabled
            if (options.disableZoom) {
                return;
            }

            var $zoomIn = this.$zoomIn;
            var $zoomOut = this.$zoomOut;

            // Bind zoom in/out
            // Don't bind one without the other
            if ($zoomIn.length && $zoomOut.length) {
                // preventDefault cancels future mouse events on touch events
                $zoomIn.on(str_click, function(e) {
                    e.preventDefault();
                    self.zoom();
                });
                $zoomOut.on(str_click, function(e) {
                    e.preventDefault();
                    self.zoom(true);
                });
            }

            if ($zoomRange.length) {
                events = {};
                // Cannot prevent default action here
                events[ str_down ] = function() {
                    self.transition(true);
                };
                // Zoom on input events if available and change events
                // See https://github.com/timmywil/jquery.panzoom/issues/90
                events[ (supportsInputEvent ? 'input' : 'change') + ns ] = function() {
                    self.zoom(+this.value, { noSetRange: true });
                };
                $zoomRange.on(events);
            }
        },

        /**
         * Unbind all events
         * @private
         */
        _unbind: function() {
            this.$elem
                .add(this.$zoomIn)
                .add(this.$zoomOut)
                .add(this.$reset)
                .off(this.options.eventNamespace);
        },

        /**
         * Builds the original transform value
         * @private
         */
        _buildTransform: function() {
            // Save the original transform
            // Retrieving this also adds the correct prefixed style name
            // to jQuery's internal $.cssProps
            return this._origTransform = this.getTransform(this.options.startTransform);
        },

        /**
         * Set transition property for later use when zooming
         * @private
         */
        _buildTransition: function() {
            if (this._transform) {
                var options = this.options;
                this._transition = this._transform + ' ' + options.duration + 'ms ' + options.easing;
            }
        },

        /**
         * Calculates the distance between two touch points
         * Remember pythagorean?
         * @param {Array} touches
         * @returns {Number} Returns the distance
         * @private
         */
        _getDistance: function(touches) {
            var touch1 = touches[0];
            var touch2 = touches[1];
            return Math.sqrt(Math.pow(Math.abs(touch2.clientX - touch1.clientX), 2) + Math.pow(Math.abs(touch2.clientY - touch1.clientY), 2));
        },

        /**
         * Constructs an approximated point in the middle of two touch points
         * @returns {Object} Returns an object containing pageX and pageY
         * @private
         */
        _getMiddle: function(touches) {
            var touch1 = touches[0];
            var touch2 = touches[1];
            return {
                clientX: ((touch2.clientX - touch1.clientX) / 2) + touch1.clientX,
                clientY: ((touch2.clientY - touch1.clientY) / 2) + touch1.clientY
            };
        },

        /**
         * Trigger a panzoom event on our element
         * The event is passed the Panzoom instance
         * @param {String|jQuery.Event} event
         * @param {Mixed} arg1[, arg2, arg3, ...] Arguments to append to the trigger
         * @private
         */
        _trigger: function (event) {
            if (typeof event === 'string') {
                event = 'panzoom' + event;
            }
            this.$elem.triggerHandler(event, [this].concat(slice.call(arguments, 1)));
        },

        /**
         * Starts the pan
         * This is bound to mouse/touchmove on the element
         * @param {jQuery.Event} event An event with pageX, pageY, and possibly the touches list
         * @param {TouchList} [touches] The touches list if present
         * @private
         */
        _startMove: function(event, touches) {
            if (this.panning) {
                return;
            }
            var moveEvent, endEvent,
                startDistance, startScale, startMiddle,
                startPageX, startPageY, touch;
            var self = this;
            var options = this.options;
            var ns = options.eventNamespace;
            var matrix = this.getMatrix();
            var original = matrix.slice(0);
            var origPageX = +original[4];
            var origPageY = +original[5];
            var panOptions = { matrix: matrix, animate: 'skip' };
            var type = event.type;

            // Use proper events
            if (type === 'pointerdown') {
                moveEvent = 'pointermove';
                endEvent = 'pointerup';
            } else if (type === 'touchstart') {
                moveEvent = 'touchmove';
                endEvent = 'touchend';
            } else if (type === 'MSPointerDown') {
                moveEvent = 'MSPointerMove';
                endEvent = 'MSPointerUp';
            } else {
                moveEvent = 'mousemove';
                endEvent = 'mouseup';
            }

            // Add namespace
            moveEvent += ns;
            endEvent += ns;

            // Remove any transitions happening
            this.transition(true);

            // Indicate that we are currently panning
            this.panning = true;

            // Trigger start event
            this._trigger('start', event, touches);

            var setStart = function(event, touches) {
                if (touches) {
                    if (touches.length === 2) {
                        if (startDistance != null) {
                            return;
                        }
                        startDistance = self._getDistance(touches);
                        startScale = self.getScale(matrix);
                        startMiddle = self._getMiddle(touches);
                        return;
                    }
                    if (startPageX != null) {
                        return;
                    }
                    if ((touch = touches[0])) {
                        startPageX = touch.pageX;
                        startPageY = touch.pageY;
                    }
                }
                if (startPageX != null) {
                    return;
                }
                startPageX = event.pageX;
                startPageY = event.pageY;
            };

            setStart(event, touches);

            var move = function(e) {
                var coords;
                e.preventDefault();
                touches = e.touches || e.originalEvent.touches;
                setStart(e, touches);

                if (touches) {
                    if (touches.length === 2) {

                        // Calculate move on middle point
                        var middle = self._getMiddle(touches);
                        var diff = self._getDistance(touches) - startDistance;

                        // Set zoom
                        self.zoom(diff * (options.increment / 100) + startScale, {
                            focal: middle,
                            matrix: matrix,
                            animate: 'skip'
                        });

                        // Set pan
                        self.pan(
                            +matrix[4] + middle.clientX - startMiddle.clientX,
                            +matrix[5] + middle.clientY - startMiddle.clientY,
                            panOptions
                        );
                        startMiddle = middle;
                        return;
                    }
                    coords = touches[0] || { pageX: 0, pageY: 0 };
                }

                if (!coords) {
                    coords = e;
                }

                self.pan(
                    origPageX + coords.pageX - startPageX,
                    origPageY + coords.pageY - startPageY,
                    panOptions
                );
            };

            // Bind the handlers
            $(document)
                .off(ns)
                .on(moveEvent, move)
                .on(endEvent, function(e) {
                    e.preventDefault();
                    // Unbind all document events
                    $(this).off(ns);
                    self.panning = false;
                    // Trigger our end event
                    // Simply set the type to "panzoomend" to pass through all end properties
                    // jQuery's `not` is used here to compare Array equality
                    e.type = 'panzoomend';
                    self._trigger(e, matrix, !matrixEquals(matrix, original));
                });
        }
    };

    // Add Panzoom as a static property
    $.Panzoom = Panzoom;

    /**
     * Extend jQuery
     * @param {Object|String} options - The name of a method to call on the prototype
     *  or an object literal of options
     * @returns {jQuery|Mixed} jQuery instance for regular chaining or the return value(s) of a panzoom method call
     */
    $.fn.panzoom = function(options) {
        var instance, args, m, ret;

        // Call methods widget-style
        if (typeof options === 'string') {
            ret = [];
            args = slice.call(arguments, 1);
            this.each(function() {
                instance = $.data(this, datakey);

                if (!instance) {
                    ret.push(undefined);

                    // Ignore methods beginning with `_`
                } else if (options.charAt(0) !== '_' &&
                    typeof (m = instance[ options ]) === 'function' &&
                    // If nothing is returned, do not add to return values
                    (m = m.apply(instance, args)) !== undefined) {

                    ret.push(m);
                }
            });

            // Return an array of values for the jQuery instances
            // Or the value itself if there is only one
            // Or keep chaining
            return ret.length ?
                (ret.length === 1 ? ret[0] : ret) :
                this;
        }

        return this.each(function() { new Panzoom(this, options); });
    };

    return Panzoom;
}));